package com.ggj2013.hbh.utils;

import nme.display.BitmapData;
import nme.display.BitmapDataChannel;
import nme.display.BlendMode;
import nme.display.DisplayObject;	
import nme.display.DisplayObjectContainer;
import nme.geom.Matrix;
import nme.geom.Point;	
import nme.geom.Rectangle;
import flash.Vector;
	
/**
 * PixelPerfectCollisionDetection 
 * handles collision between the bitmaps of different objects
 * as opposed to their hit rectangles.
 */
	
/**
 * Source: http://troygilbert.com/2007/06/pixel-perfect-collision-detection-in-actionscript3/
 * "The code is totally okay to share. I didn't stick a license on it, but you can consider it equivalent
 * to MIT or LPGL... free to do with it what you want, commercial or personal, just don't claim a verboten
 * version as your own. Attribution is always appreciated but not required." --Troy Gilbert 
 */
class CollisionDetection
{
	/* KNOWN ISSUES WITH IOS BUILDS:
	 * -BitmapData.draw() function does not pick up alpha of Sprite if Sprite.visible is false
	 * -When a Sprite with full alpha is rendered to a BitmapData, the BitmapData's alpha is 254 (i.e. not full)
	 */

	/**
     * Precalculated coefficient used to convert degress to radians.
     */
    public static inline var DEGREES_TO_RADIANS:Float = Math.PI / 180.0;
	
	/** Get the collision rectangle between two display objects. **/
	public static function getCollisionRect(target1:DisplayObject, target2:DisplayObject, commonParent:DisplayObjectContainer, pixelPrecise:Bool = false, tolerance:Float = 0):Rectangle
	{
		// get bounding boxes in common parent's coordinate space
		var rect1:Rectangle = target1.getBounds(commonParent);
		var rect2:Rectangle = target2.getBounds(commonParent);
		// find the intersection of the two bounding boxes
		var intersectionRect:Rectangle = rect1.intersection(rect2);
		if (intersectionRect.size.length> 0)
		{
			if (pixelPrecise)
			{
				// size of rect needs to integer size for bitmap data
				intersectionRect.width = Math.ceil(intersectionRect.width);
				intersectionRect.height = Math.ceil(intersectionRect.height);
				// get the alpha maps for the display objects
				var alpha1:BitmapData = getAlphaMap(target1, intersectionRect, BitmapDataChannel.RED, commonParent);
				var alpha2:BitmapData = getAlphaMap(target2, intersectionRect, BitmapDataChannel.GREEN, commonParent);
				// combine the alpha maps
				alpha1.draw(alpha2, null, null, BlendMode.LIGHTEN);

				// calculate the search color
				var searchColor:UInt;
				
				if (tolerance <= 0)
				{
					searchColor = 0x010100;
				}
				else
				{
					if (tolerance> 1) tolerance = 1;
					var byte:Int = Math.round(tolerance * 255);
					searchColor = (byte <<16) | (byte <<8) | 0;
				}
				// find color
				var collisionRect:Rectangle = alpha1.getColorBoundsRect(searchColor, searchColor);
				collisionRect.x += intersectionRect.x;
				collisionRect.y += intersectionRect.y;
				return collisionRect;
			}
			else
			{
				return intersectionRect;
			}
		}
		else
		{
			// no intersection
			return null;
		}
	}
	
	/** Gets the alpha map of the display object and places it in the specified channel. **/
	//TODO: below works on Android
	//private static function getAlphaMap(target:DisplayObject, rect:Rectangle, channel:UInt, commonParent:DisplayObjectContainer):BitmapData
	private static function getAlphaMap(target:DisplayObject, rect:Rectangle, channel:Int, commonParent:DisplayObjectContainer):BitmapData
	{
		// calculate the transform for the display object relative to the common parent
		var parentXformInvert:Matrix = commonParent.transform.concatenatedMatrix.clone();
		parentXformInvert.invert();
		var targetXform:Matrix = target.transform.concatenatedMatrix.clone();
		targetXform.concat(parentXformInvert);
		// translate the target into the rect's space
		targetXform.translate(-rect.x, -rect.y);
		// draw the target and extract its alpha channel into a color channel
		var bitmapData:BitmapData = new BitmapData(Std.int(rect.width), Std.int(rect.height), true, 0);
		#if debug
			var l_sx:Float = target.scaleX; var l_sy:Float = target.scaleY;
		#end
		bitmapData.draw(target, targetXform);
		#if debug
			if (target.scaleX != l_sx || target.scaleX != l_sy)
			{
				//trace("Warning: scale values disoriented for '"+target.toString()+"'");
			}
		#end
		var alphaChannel:BitmapData = new BitmapData(Std.int(rect.width), Std.int(rect.height), false, 0);
		alphaChannel.copyChannel(bitmapData, bitmapData.rect, new Point(0, 0), BitmapDataChannel.ALPHA, channel);
		return alphaChannel;
	}
	
	public static function GetBoundingRectangle(object:DisplayObject):Rectangle 
	{
		var rotation = object.rotation;
		if ( rotation < 0 ) 
		{
			rotation = 360 + rotation;
		}

		var position = new Point(object.x, object.y);
		if ((rotation > 0) && (rotation < 360)) 
		{
			if (rotation < 90) 
			{
				position.x -= object.height * Math.sin(rotation);
			}
			else if (rotation == 90) 
			{
				position.x -= object.width;
			}
			else if (rotation < 180) 
			{
				position.x -= object.width;
				position.y -= object.width * Math.sin(rotation);
			}
			else if (rotation == 180) 
			{
				position.x -= object.width;
				position.y -= object.height;
			}
			else if (rotation < 270) 
			{
				position.x -= object.height * Math.sin(rotation);
				position.y -= object.height;
			}
			else if (rotation == 270) 
			{
				position.y -= object.height;
			}
			else 
			{
				position.y -= object.width * Math.sin(rotation);
			}
		}
		return ( new Rectangle( position.x, position.y, object.width, object.height ) );
	}
	
	/** Get the center of the collision's bounding box. **/
	public static function getCollisionPoint(target1:DisplayObject, target2:DisplayObject, commonParent:DisplayObjectContainer, pixelPrecise:Bool = false, tolerance:Float = 0):Point
	{
		var collisionRect:Rectangle = getCollisionRect(target1, target2, commonParent, pixelPrecise, tolerance);
		if (collisionRect != null && collisionRect.size.length> 0)
		{
			var x:Float = (collisionRect.left + collisionRect.right) / 2;
			var y:Float = (collisionRect.top + collisionRect.bottom) / 2;
			return new Point(x, y);
		}
		return null;
	}
	
	/** Are the two display objects colliding (overlapping)? **/
	public static function isPixelPerfectColliding(target1:DisplayObject, target2:DisplayObject, commonParent:DisplayObjectContainer, pixelPrecise:Bool = false, tolerance:Float = 0):Bool
	{
		var collisionRect:Rectangle = getCollisionRect(target1, target2, commonParent, pixelPrecise, tolerance);
		
		if ( collisionRect != null && collisionRect.size.length > 0 ) 
		{
			return true;
		}
		else return false;
	}
	
	public static function isSimpleColliding( target1:DisplayObject, target2:DisplayObject, commonParent:DisplayObjectContainer, pixelPrecise:Bool = false, tolerance:Float = 0 ):Bool
	{	
		var l_colWidth1:Int = Std.int(target1.width * 0.80);
		var l_colHeight1:Int = Std.int(target1.height * 0.80);
		
		var l_colXOffset1:Int = Std.int(target1.width - l_colWidth1 / 2);
		var l_colYOffset1:Int = Std.int(target1.height - l_colHeight1 / 2);
		
		var l_colWidth2:Int = Std.int(target2.width * 0.80);
		var l_colHeight2:Int = Std.int(target2.height * 0.80);
		
		var l_colXOffset2:Int = Std.int(target2.width - l_colWidth2 / 2);
		var l_colYOffset2:Int = Std.int(target2.height - l_colHeight2 / 2);
		
		var left1:Int;
		var left2:Int;
		var right1:Int;
		var right2:Int;
		var top1:Int;
		var top2:Int;
		var bottom1:Int;
		var bottom2:Int;

        left1 = Std.int(target1.x + l_colXOffset1);
        left2 = Std.int(target2.x + l_colXOffset2);
        right1 = left1 + l_colWidth1;
        right2 = left2 + l_colWidth2;
        top1 = Std.int(target1.y + l_colYOffset1);
        top2 = Std.int(target2.y + l_colYOffset1);
        
        bottom1 = top1 + l_colHeight1;
        bottom2 = top2 + l_colHeight2;

        if (bottom1 < top2)
        {
        	return false;
        }
        if (top1 > bottom2)
        {
        	return false;
        }
        if (right1 < left2)
        {
        	return false;
        }
        if (left1 > right2)
        {
        	return false;
        }

        return true;
	}
	
	public static function isSimpleYColliding(target1:DisplayObject, target2:DisplayObject, commonParent:DisplayObjectContainer, pixelPrecise:Bool = false, tolerance:Float = 0):Bool
	{	
		var l_colHeight1:Int = Std.int(target1.height * 0.80);
		var l_colYOffset1:Int = Std.int(target1.height - l_colHeight1 / 2);
		
		var l_colHeight2:Int = Std.int(target2.height * 0.80);
		var l_colYOffset2:Int = Std.int(target2.height - l_colHeight2 / 2);
		
		var top1:Int;
		var top2:Int;
		var bottom1:Int;
		var bottom2:Int;

        top1 = Std.int(target1.y + l_colYOffset1);
        top2 = Std.int(target2.y + l_colYOffset1);
        
        bottom1 = top1 + l_colHeight1;
        bottom2 = top2 + l_colHeight2;

        if (bottom1 < top2)
        {
        	return false;
        }
        if (top1 > bottom2)
        {
        	return false;
        }
        return true;
	}
 
    public static inline function getRotatedBounds(rect:Rectangle, angle:Float, ?targetRect:Rectangle=null, fracOffsetX:Float=0.5, fracOffsetY:Float=0.5 ):Rectangle 
    {
        if (targetRect == null)
        {
        	targetRect = new Rectangle();
        }
 
        var width:Float = rect.width;
        var height:Float = rect.height;
 
        angle = modulo(angle, 360);
        var rad = angle * DEGREES_TO_RADIANS;
        var absSinA:Float = Math.abs(Math.sin(rad));
        var absCosA:Float = Math.abs(Math.cos(rad));
        var widthSin = width * absSinA;
        var widthCos = width * absCosA;
        var heightSin = height * absSinA;
        var heightCos = height * absCosA;
 
        targetRect.left = rect.x - fracOffsetX * (widthCos + heightSin);
        targetRect.right = rect.x + (1 - fracOffsetX) * (widthCos + heightSin);
        targetRect.top = rect.y - fracOffsetY * (widthSin + heightCos);
        targetRect.bottom = rect.y + (1 - fracOffsetY) * (widthSin + heightCos);
 
        var targetWidth = targetRect.width;
        var targetHeight = targetRect.height;
        var offsetX:Float = 0.0;
        var offsetY:Float = 0.0;
 
        if (angle < 90) 
        {
        	offsetX -= heightSin;
        } 
        else if (angle == 90) 
        {
        	offsetX -= targetWidth;
        } 
        else if (angle < 180) 
        {
        	offsetX -= targetWidth;
            offsetY -= heightCos;
        } 
        else if (angle == 180) 
        {
        	offsetX -= targetWidth;
            offsetY -= targetHeight;
        } 
        else if (angle < 270) 
        {
        	offsetX -= widthCos;
        	offsetY -= targetHeight;
        } 
        else if (angle == 270) 
        {
        	offsetY -= targetHeight;
        } 
        else 
        {
        	offsetY -= widthSin;
        }
        targetRect.x += offsetX + fracOffsetX * targetWidth;
        targetRect.y += offsetY + fracOffsetY * targetHeight;
        return targetRect;
    }
    
    /**
     * Accumulate simple additive rotation.
     */
    public static inline function getAccumulatedRotation(context:DisplayObject, ?referenceContext:DisplayObject=null, ?depth:Int = 256):Float 
    {
    	var accumulatedRotation:Float = context.rotation;
    	var l = 0;
    	while (((context = context.parent) != null) || l > depth) 
    	{
        	accumulatedRotation += context.rotation;
            if (referenceContext != null && context == referenceContext) 
            {
            	break;
            }
            l++;
        }
        return accumulatedRotation;
	}
 
    /**
     * Scale can be accumulated through multiplication.
     */
    public static inline function getAccumulatedScale(context:DisplayObject, ?referenceContext:DisplayObject=null, ?depth:Int = 256):Point 
    {
		var accumulatedScale:Point = new Point(context.scaleX, context.scaleY);
		var l = 0;
		while (((context = context.parent) != null) || l > depth) 
		{
			accumulatedScale.x *= context.scaleX;
			accumulatedScale.y *= context.scaleY;
			if (referenceContext != null && context == referenceContext) 
			{
				break;
			}
            l++;
        }
        return accumulatedScale;
	}
 
 	public static inline function transformBetweenContexts(context:DisplayObject, referenceContext:DisplayObject, ?p:Point=null):Point 
 	{
		if (p == null) 
		{
			p = new Point( 0, 0 );
		}
        return referenceContext.globalToLocal(context.localToGlobal(p));
	}
    
    public static inline function getBounds(context:DisplayObject, referenceContext:DisplayObject):Rectangle 
    {
    	var rect;
        // get unrotated bounds.
        var r = context.rotation;
        context.rotation = 0;
        var w = context.width;
        var h = context.height;
        context.rotation = r;
 
        // transform context origin to reference space.
        var p = transformBetweenContexts(context, referenceContext);
 
        // initialize rectangle at current position and unrotated dimensions.
        rect = new Rectangle(p.x, p.y, w, h);
 
        // account for scale.
        var scale = getAccumulatedScale(context.parent);
        rect.width *= scale.x;
        rect.height *= scale.y;
 
        // compute AABB for rotation.
        //var l_angle:Float = getAccumulatedRotation(context);
        getRotatedBounds( rect.clone(), getAccumulatedRotation(context), rect );
        
        return rect;
	}
 
    /**
     * Donald Knuth mod algorithm
     */
    public static inline function modulo(value:Float, divisor:Float):Float 
    {
    	var q = Math.floor(value / divisor);
    	return value - q * divisor;
    }
	
	/**
	 * Combines the two alpha maps to check for overlap later.
	 * This is a replacement function for <code>alpha1.draw(alpha2, null, null, "LIGHTEN");</code> in
	 * the getCollisionRect function, since the "LIGHTEN" param is not implemented in NME as of 9/19/12.
	 * @param alpha1 - the destination BitmapData
	 * @param alpha2 - the source BitmapData
	 */
	private static function combineAlphaMaps(alpha_dst:BitmapData, alpha_src:BitmapData)
	{
		var vd:Vector<Int> = alpha_dst.getVector( alpha_dst.rect ); //TODO: report bug on forums: red channel becomes full when BMD has no alpha channel or full alpha (test if alpha on part of BMD affects only that part of BMD)
		var vs:Vector<Int> = alpha_src.getVector( alpha_src.rect );
		var size:Int = alpha_src.width * alpha_src.height;
		var srcC:Int; var dstC:Int; var newPixel:Int;
		for ( i in 0...size )
		{
			//newPixel = 0x7f000000; //for debugging so that we can view the combined bitmap
			newPixel = 0;
			
			//red
			srcC = vs[i] & 0xff0000;
			dstC = vd[i] & 0xff0000;
			newPixel += srcC > dstC ? srcC : dstC;
			
			//green
			srcC = vs[i] & 0x00ff00;
			dstC = vs[i] & 0x00ff00;
			newPixel += srcC > dstC ? srcC : dstC;
			
			//blue
			srcC = vs[i] & 0x0000ff;
			dstC = vs[i] & 0x0000ff;
			newPixel += srcC > dstC ? srcC : dstC;
			
			//Make color components odd for getColorBoundsRect() call later
			newPixel = newPixel & 0xff0000 != 0 ? newPixel | 0x010000 : newPixel;
			newPixel = newPixel & 0x00ff00 != 0 ? newPixel | 0x000100 : newPixel;
			newPixel = newPixel & 0x0000ff != 0 ? newPixel | 0x000001 : newPixel;
			
			vd[i] = newPixel;
		}
		alpha_dst.setVector( alpha_dst.rect, vd );
		
	}

}